package com.zws.cucumber.expander;

import com.zws.cucumber.expr.ZwsExprUtils;
import com.zws.cucumber.util.colorfullog.ZwsColorfulLogger;
import com.zws.cucumber.var.ZwsVar;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

import javax.script.ScriptContext;
import javax.script.SimpleScriptContext;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.zws.cucumber.expander.ZwsStrConst.*;
import static com.zws.cucumber.expander.ZwsStrInternalUtil.getJsonPath;
import static com.zws.cucumber.expr.ZwsExprBuiltInGenerators.engine;
import static com.zws.cucumber.util.colorfullog.ZwsColorfulLogHelper.blue;

/**
 * author: zws
 */
public class ZwsStrExpander extends ZwsStrBasicExpander {

    public static final Object ParamEmptyString = new Object() {
    @Override
    public String toString() {
        return "<ParamEmptyString>";
    }
};


    private static final Logger logger = ZwsColorfulLogger.of(ZwsStrExpander.class);

    private final Map<String, Object> localVariables;

    public static String expand(String input, ZwsStrBasicExpander parent, boolean debug, int level, Map<String, Object> localVariables) {
        return expand(input, parent, debug, level, localVariables, false);
    }

    private static String expand(String input, ZwsStrBasicExpander parent, boolean debug, int level, Map<String, Object> localVariables, boolean insideScript) {
        try {
            ZwsStrExpander expander = null;
            String output = input != null ? (expander = new ZwsStrExpander(input, parent, debug, level, localVariables)).expand(insideScript) : null;
            if (!Objects.equals(input, output)
                    && (parent == null || parent.debug || debug)) {
                int level0 = expander.level() + 1;
                if (input.contains("\n") || input.contains("\r") || output.contains("\n") || output.contains("\r")) {
                    logger.debug("expand <b>#{} </b>\n<b>[</b>\n{}\n<b>] --------> [</b>\n{}\n<b>]</b>",
                            level0,
                            input,
                            output);
                } else {
                    logger.debug("expand <b>#{} [</b>{}<b>] --> [</b>{}<b>]</b>",
                            level0,
                            input,
                            output);
                }
            }
            return output;
        } catch (Throwable t) {
            logger.debug("expand <r>[</r>{}<r>]</r>", input);
            throw t;
        }
    }

    public ZwsStrExpander(String input, ZwsStrBasicExpander parent, boolean debug, int level, Map<String, Object> localVariables) {
        super(input, parent, debug, level);
        this.localVariables = localVariables;
    }

    public String expand() {
        return expand(false);
    }

    public String expand(boolean insideScript) {
        parseOption();
        StringBuilder outputBuf = new StringBuilder();
        while (index < input.length()) {
            if (!insideScript && expectCurrent(SYMBOL_BACKTICK) && !expectNext(SYMBOL_BACKTICK)) {
                outputBuf.append(resolve(debug, this::resolveScript));

            } else if (expectCurrent(SYMBOL_DOLLAR, SYMBOL_AT, SYMBOL_SHARP) && expectNext(SYMBOL_BRACKETS_LEFT)) {
                outputBuf.append(resolve(debug, this::resolveFuncOrPropOrVar));

            } else {
                /* escape ` */
                if (!insideScript && expectCurrent(SYMBOL_BACKTICK)) {
                    index++;
                }
                outputBuf.append(charAt(index++));
            }
        }
        return prettyPrintIfNeeded(outputBuf.toString());
    }

    private Object resolve(boolean debug, Function<Boolean, Object> resolveFunc) {
        increaseLevel();
        markStartIndex();
        try {
            return resolveFunc.apply(debug);
        } catch (Exception e) {
            throw new RuntimeException(getExceptionPositionMessage(), e);
        } finally {
            unmarkStartIndex();
            decreaseLevel();
        }
    }

    private Object resolveScript(boolean debug) {
        State state = new State();

        match(SYMBOL_BACKTICK);
        state.setType(S_TYPE_SCRIPT);
        StringBuilder scriptBuf = new StringBuilder();
        while (!expectCurrent(SYMBOL_BACKTICK) || (expectCurrent(SYMBOL_BACKTICK) && expectNext(SYMBOL_BACKTICK) && index + 1 < length)) {
            if (expectCurrent(SYMBOL_BACKTICK)) {
                index++;
            }
            scriptBuf.append(charAt(index++));
        }
        match(SYMBOL_BACKTICK);

        String script = scriptBuf.toString();
        script = ZwsStrExpander.expand(script, this, debug, 0, localVariables, true);
        ScriptContext scriptContext = new SimpleScriptContext();
        ZwsExprUtils.pushVarBlockAndFillAttrs(scriptContext);
        try {
            assert engine != null;
            logger.debug(label(state, script));
            Object object = engine.eval(script, scriptContext);
            return object != null ? object.toString() : "";
        } catch (Throwable t) {
            logger.debug("", t);
            throw new RuntimeException(t);
        } finally {
            ZwsVar.popProcedureBlock();
        }
    }

    private Object resolveFuncOrPropOrVar(boolean debug) {
//        int state = 0;
        State state = new State();
        StringBuilder keyBuf = new StringBuilder();
        StringBuilder outputBuf = new StringBuilder();
        List<Object> argList = new ArrayList<>();

        if (expectCurrent(SYMBOL_AT)) {
            match(SYMBOL_AT);
            state.setType(S_TYPE_VAR);

        } else if (expectCurrent(SYMBOL_DOLLAR)) {
            match(SYMBOL_DOLLAR);
            state.setType(S_TYPE_PROP);

        } else if (expectCurrent(SYMBOL_SHARP)) {
            match(SYMBOL_SHARP);
            state.setType(S_TYPE_FUNC);

        } else {
            throw new IllegalStateException();
        }
        match(SYMBOL_BRACKETS_LEFT);
        skipWhiteSpaces();
        if (expectCurrent(SYMBOL_EXCLAMATION)) {
            debug = true;
//            logger.debug("{}, enable debug", resolveString(state));
            match(SYMBOL_EXCLAMATION);
        }
        skipWhiteSpaces();
        // the key word
        if (ZwsStrParseUtil.isKeyCharacter(charAt(index))) {
            while (ZwsStrParseUtil.isKeyCharacter(charAt(index))) {
                keyBuf.append(charAt(index++));
            }
        } else {
            throw new IllegalArgumentException("Syntax of " + state + " is invalid. Parse info stack: \n" + generateParseStack());
        }
        String key = keyBuf.toString();
//        if (debug) { logger.debug("{}, key: {}{}{}", label(state, key), blue("["), keyBuf, blue("]")); }
        // default value
        skipWhiteSpaces();
        String defaultValue = null;
        String jsonPath = null;

        if (expectCurrent(SYMBOL_QUESTION)) {
            // default value
            if (state.isType(S_TYPE_FUNC)) {
                throw new IllegalArgumentException("Syntax of " + resolveWhat(state) + " is invalid. "
                        + "Function must not have default value. "
                        + "Parse info stack: \n" + generateParseStack());
            }
            if (debug) { logger.debug("{}, have default value", label(state, key)); }
            state.setSpec(S_SPEC_DEFAULT_VALUE);
            match(SYMBOL_QUESTION);
            skipWhiteSpaces();
            defaultValue = scanAndExpandDefaultValue(debug);

        } else if (expectCurrent(SYMBOL_DOLLAR)) {
            // json path
            if (state.isType(S_TYPE_FUNC)) {
                throw new IllegalArgumentException("Syntax of " + resolveWhat(state) + " is invalid. "
                        + "Function must not have json path. "
                        + "Parse info stack: \n" + generateParseStack());
            }
            if (debug) { logger.debug("{}, have json path", label(state, key)); }
            state.setSpec(S_SPEC_JSON_PATH);
            match(SYMBOL_DOLLAR);
            StringBuilder jsonPathSb = new StringBuilder();
            jsonPathSb.append(SYMBOL_DOLLAR);
            while (!expectCurrent(SYMBOL_BRACKETS_RIGHT)) {
                jsonPathSb.append(charAt(index++));
            }
            jsonPath = jsonPathSb.toString().trim();
            if (debug) { logger.debug("{}, json path: {}", label(state, key), jsonPath); }

        } else if (expectCurrent(SYMBOL_SEMICOLON)) {
            // template & function
            if (!state.isType(S_TYPE_FUNC) && (state.isSpec(S_SPEC_DEFAULT_VALUE) || state.isSpec(S_SPEC_JSON_PATH))) {
                throw new IllegalArgumentException("Syntax of " + resolveWhat(state) + " is invalid. "
                        + "Property/Variable with default_value(?)/json_path($) must not have arguments. "
                        + "Parse info stack: \n" + generateParseStack());
            }
            if (debug) { logger.debug("{}, have arguments", label(state, key)); }
            state.setSpec(S_SPEC_TEMPLATE);
            match(SYMBOL_SEMICOLON);
            skipWhiteSpaces();
            argList.add(scanAndExpandOrderedArgument(debug));
            skipWhiteSpaces();
            while (expectCurrent(SYMBOL_COMMA)) {
                match(SYMBOL_COMMA);
                skipWhiteSpaces();
                argList.add(scanAndExpandOrderedArgument(debug));
                skipWhiteSpaces();
            }
            if (debug) { logger.debug("{}, arguments resolved. {}{}{}", label(state, key), blue("["), argList.stream().map(o -> (Objects.nonNull(o) ? o.getClass().getSimpleName() : "null") + ":" + o).collect(Collectors.joining(", ")), blue("]")); }
        }
        skipWhiteSpaces();
        match(SYMBOL_BRACKETS_RIGHT);

        Object value = resolveKey(key, state, debug);

        if (state.isType(S_TYPE_FUNC)) {
            if (value != null) {
                Object returnValue = ZwsStrFunctionUtil.functionMap.get(value.toString()).apply(
                        debug,
                        argList.stream().map(v -> v == ParamEmptyString ? "" : v).toArray());
//                        argList.toArray(new Object[0]));
                if (debug) { logger.debug("{}, return value from function, {}{}{}{}{}", label(state, key), blue("value: ["), returnValue, blue("], type:["), returnValue != null ? returnValue.getClass().getSimpleName() : null, blue("]")); }
                return returnValue;
            }

        } else {
            // S_TYPE_PROP, S_TYPE_VAR
            switch (state.getSpec()) {
                case S_SPEC_NONE, S_SPEC_DEFAULT_VALUE -> {
                    if (value == null || value == ParamEmptyString) {
                        if (state.isSpec(S_SPEC_DEFAULT_VALUE)) {
                            value = defaultValue;
                        }
                        if (value == ParamEmptyString) {
                            return ParamEmptyString;
                        }

                    }
                    if (value != null) {
                        // value that is resolved or default value
                        if (Objects.equals("${" + key + "}", value) || Objects.equals("@{" + key + "}", value)) {
                            return value;
                        } else {
                            return expand(value.toString(), this, debug, 0, localVariables);
                        }
                    }


                }
                case S_SPEC_JSON_PATH -> {
                    if (value != null) {
                        if (StringUtils.isBlank(jsonPath)) {
                            throw new IllegalArgumentException("json path should not be blank");
                        }
                        if (value instanceof String) {
                            return getJsonPath(jsonPath).read((String) value);
                        } else {
                            return getJsonPath(jsonPath).read(value);
                        }
                    }
                }
                case S_SPEC_TEMPLATE -> {
                    if (value != null) {
                        Map<String, Object> argMap = new HashMap<>();
                        for (int i = 0; i < argList.size(); i++) {
                            argMap.put(Integer.toString(i), argList.get(i));
                        }
//                        value = ZwsStrTemplateExpander.expandTemplate(value.toString(), this, debug, 0, argMap, null);
//                        return expand(value.toString(), this, debug, 0, localVariables);
                        return expand(
                                value.toString(),
                                this,
                                debug,
                                0,
                                new ZwsStrArgFirstMap(argMap, localVariables));
                    }
                }
            }
        }

        outputBuf.append(
                        state.isType(S_TYPE_VAR)
                                ? SYMBOL_AT
                                : state.isType(S_TYPE_PROP) ? SYMBOL_DOLLAR : SYMBOL_SHARP)
                .append(SYMBOL_BRACKETS_LEFT).append(keyBuf);
        switch (state.getSpec()) {
            case S_SPEC_DEFAULT_VALUE -> outputBuf.append(SYMBOL_QUESTION).append(defaultValue);
            case S_SPEC_JSON_PATH -> outputBuf.append(jsonPath);
            case S_SPEC_TEMPLATE ->
                    outputBuf.append(SYMBOL_SEMICOLON)
                            .append(argList
                                    .stream()
                                    .map(Object::toString)
                                    .collect(Collectors.joining(Character.toString(SYMBOL_COMMA))));
        }

        outputBuf.append(SYMBOL_BRACKETS_RIGHT);
        String originalInput = outputBuf.toString();
        if (debug) { logger.debug("{}, return original input, {}{}{}", label(state, key), blue("value: ["), originalInput, blue("]")); }
        return originalInput;

    }

    // the arguments are separated by ',' which between ':' and '}' without heading and tailing whitespace.
    protected Object scanAndExpandOrderedArgument(boolean debug) {
        StringBuilder argBuf = new StringBuilder();
        boolean nonNull = false;
        Object retObj = null;
        int endIndex = 0;
        skipWhiteSpaces();

        // escape '
        boolean startWithQuotation = false;
        if (expectCurrent(SYMBOL_SINGLE_QUOTATION)) {
            match(SYMBOL_SINGLE_QUOTATION);
            startWithQuotation = true;
        }

        while ((!startWithQuotation && !expectCurrent(SYMBOL_COMMA, SYMBOL_BRACKETS_RIGHT))
                || (startWithQuotation && (!expectCurrent(SYMBOL_SINGLE_QUOTATION) || expectNext(SYMBOL_SINGLE_QUOTATION)))) {

            if ((expectCurrent(SYMBOL_AT) || expectCurrent(SYMBOL_DOLLAR) || expectCurrent(SYMBOL_SHARP))
                    && expectNext(SYMBOL_BRACKETS_LEFT)) {
                retObj = resolve(debug, this::resolveFuncOrPropOrVar);
                if (retObj != null) {
                    nonNull = true;
                }
                if (retObj instanceof String || !argBuf.isEmpty()) {
                    argBuf.append(retObj);
                    retObj = null;
                }
                endIndex = argBuf.length();

            } else {
                if (startWithQuotation && expectCurrent(SYMBOL_SINGLE_QUOTATION)) {
                    match(SYMBOL_SINGLE_QUOTATION); // only '' can come here
                }
                char c = charAt(index++);
                if (retObj != null) {
                    argBuf.append(retObj);
                    retObj = null;
                }
                argBuf.append(c);
                if (!Character.isWhitespace(c)) {
                    endIndex = argBuf.length();
                }
            }
        }
        if (startWithQuotation) {
            match(SYMBOL_SINGLE_QUOTATION);
        }

        /*
         * return retObj if no other character preceding or appending
         */
        if (retObj != null) {
            if (argBuf.isEmpty()) {
                return retObj;
            } else {
                argBuf.append(retObj);
            }
        }
        return startWithQuotation
                ? argBuf.toString()
                : endIndex > 0 || nonNull ? argBuf.substring(0, endIndex) : ParamEmptyString;

    }

    // the default value is the characters between '?' and '}' without the heading and tailing whitespace.
    private String scanAndExpandDefaultValue(boolean debug) {
        StringBuilder argBuf = new StringBuilder();
        int endIndex = 0;
        skipWhiteSpaces();
        // escape '
        if (expectCurrent(SYMBOL_SINGLE_QUOTATION)) {
            match(SYMBOL_SINGLE_QUOTATION);
            while (true) {
                if (!expectCurrent(SYMBOL_SINGLE_QUOTATION)) {
                    argBuf.append(charAt(index++));
                } else if (expectNext(SYMBOL_SINGLE_QUOTATION)) {
                    match(SYMBOL_SINGLE_QUOTATION);
                    match(SYMBOL_SINGLE_QUOTATION);
                    argBuf.append(SYMBOL_SINGLE_QUOTATION);
                } else {
                    break;
                }
            }
            match(SYMBOL_SINGLE_QUOTATION);
            return argBuf.toString();

        }

        while (!expectCurrent(SYMBOL_BRACKETS_RIGHT)) {
            if ((expectCurrent(SYMBOL_AT) || expectCurrent(SYMBOL_DOLLAR) || expectCurrent(SYMBOL_SHARP))
                    && expectNext(SYMBOL_BRACKETS_LEFT)) {
                argBuf.append(resolve(debug, this::resolveFuncOrPropOrVar));
                endIndex = argBuf.length();
            } else {
                char c = charAt(index++);
                argBuf.append(c);
                if (!Character.isWhitespace(c)) {
                    endIndex = argBuf.length();
                }
            }
        }
        //            skipWhiteSpaces();
        return argBuf.substring(0, endIndex);
    }


    private Object resolveKey(String key, State state, boolean debug) {
        Object value = null;
        if (state.isType(S_TYPE_PROP)) {
            value = getProperty(key);
            if (debug) { logger.debug("{}, return value, {}{}{}", label(state, key), blue("value: ["), value, blue("]")); }
        }
        if (state.isType(S_TYPE_VAR)) {
            value = localVariables != null ? localVariables.get(key) : ZwsVar.get(key);
            if (debug) { logger.debug("{}, return value from {}, {}{}{}", label(state, key), valueFrom(localVariables, key), blue("value: ["), value, blue("]")); }
        }
        if (state.isType(S_TYPE_FUNC) && ZwsStrFunctionUtil.functionMap.containsKey(key)) {
            value = key;
            if (debug) { logger.debug("{}, return value, {}{}{}", label(state, key), blue("value: ["), value, blue("]")); }
        }
        return value;
    }

    private boolean isKeyResolved(String key, State state, boolean debug) {
        if (state.isType(S_TYPE_PROP)) {
            return System.getProperties().containsKey(key);
        }
        if (state.isType(S_TYPE_VAR)) {
            return localVariables != null ? localVariables.containsKey(key) : ZwsVar.containsKey(key);
        }
        if (state.isType(S_TYPE_FUNC) && ZwsStrFunctionUtil.functionMap.containsKey(key)) {
            throw new UnsupportedOperationException();
        }
        return false;
    }

    private static String getProperty(String key) {
        // todo: to be implemented
        return null;
    }

    private String label(State state, String key) {
        return "resolve " + state + " " + blue("[") + key + blue("]#" + level());
    }

    private String resolveWhat(State state) {
        return switch (state.getType()) {
            case S_TYPE_VAR -> "variable";
            case S_TYPE_PROP -> "property";
            case S_TYPE_FUNC -> "function";
            case S_TYPE_SCRIPT -> "script";
            default -> "plaintext";
        };
    }

    private String valueFrom(Map<String, Object> localVariables, String key) {
        if (localVariables == null) {
            return "global";
        }
        if (localVariables instanceof ZwsStrArgFirstMap map) {
            if (map.containsInArgumentMap(key)) {
                return "argument";
            }
            if (map.containsInLocalVariables(key)) {
                return "local";
            }
        }
        return "global";
    }

    /*
     * state function
     */
    private static class State {
        private int state = 0;
        private int getType() {
            return state & 0x00FF;
        }

        private int getType(int type) {
            return type & 0x00FF;
        }

        private void setType(int type) {
            if (getType() != 0) {
                throw new IllegalStateException("original state type should be 0");
            }
            if (getType(type) == 0) {
                throw new IllegalArgumentException("target state type should not be 0");
            }
            state |= type;
        }

        private boolean isType(int type) {
            return getType() == type;
        }

        private int getSpec() {
            return state & 0xFF00;
        }

        private int getSpec(int spec) {
            return spec & 0xFF00;
        }

        private void setSpec(int spec) {
            if (getSpec() != 0) {
                throw new IllegalStateException("original state spec should be 0");
            }
            if (getSpec(spec) == 0) {
                throw new IllegalArgumentException("target state spec should not be 0");
            }
            state |= spec;
        }

        private boolean isSpec(int spec) {
            return getSpec() == spec;
        }

        @Override
        public String toString() {
            StringJoiner joiner = new StringJoiner("|", "state(", ")");
            joiner.add(switch (getType()) {
                case S_TYPE_PROP -> "PROP";
                case S_TYPE_VAR -> "VAR";
                case S_TYPE_FUNC -> "FUNC";
                case S_TYPE_SCRIPT -> "SCRIPT";
                default -> "NO_TYPE";
            });
            joiner.add(switch (getSpec()) {
                case S_SPEC_DEFAULT_VALUE -> "DEFAULT";
                case S_SPEC_JSON_PATH -> "JSONPATH";
                case S_SPEC_TEMPLATE -> "TEMPLATE";
                default -> "NO_SPEC";
            });
            return joiner.toString();
        }
    }

}
